package xj.graph2d.shapes;

import static xj.graph2d.DrawShapeConstants.PointGraphEdgeDestination;
import static xj.graph2d.DrawShapeConstants.PointGraphEdgeSource;
import static xj.graph2d.DrawShapeConstants.PointInBounds;
import static xj.graph2d.DrawShapeConstants.PointOnCurve;
import static xj.graph2d.DrawShapeConstants.PointOnCurveCtrPoint;
import static xj.graph2d.DrawShapeConstants.PointOnCurveEndPoint;
import static xj.graph2d.DrawShapeConstants.PointOutside;
import static xj.graph2d.DrawShapeConstants.anchorColor;
import static xj.graph2d.DrawShapeConstants.ctrStroke;
import static xj.graph2d.DrawShapeConstants.d;
import static xj.graph2d.DrawShapeConstants.edgeDestinationColor;
import static xj.graph2d.DrawShapeConstants.edgeSourceColor;
import static xj.graph2d.DrawShapeConstants.frameMarkFilled;
import static xj.graph2d.DrawShapeConstants.markStroke;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.HashMap;
import java.util.Map;

import xj.graph2d.DrawAttr;
import xj.graph2d.DrawShape;
import xj.graph2d.DrawShapeBase;
import xj.graph2d.GraphNodeBase;
import xj.graph2d.util.DrawingUtilities;
import xj.util.gui.Point2DFloat;

import static xj.graph2d.graph.Geometry.CHECK;

public class CubicCurveShape 
  extends CurveShapeBase {

  public static final int S_SHAPE_CURVE = 0x00;

  public static final int S_SHAPE_CURVE_INVERSE = 0x01;

  public static final int C_SHAPE_CURVE = 0x10;

  public static final int C_SHAPE_CURVE_INVERSE = 0x11;

  protected int curveType = S_SHAPE_CURVE;

  public CubicCurveShape() {
    this(false);
  }

  public CubicCurveShape(boolean createComp) {
    super(createComp);
    ctr1 = new Point2DFloat();
    ctr2 = new Point2DFloat();
  }

  public CubicCurveShape(float x1, float y1, float x2, float y2) {
    this(x1, y1, x2, y2, 0, 0, 0, 0, false);
    adjustControlPoints();
  }

  public CubicCurveShape(float x1, float y1, float x2, float y2, 
			 float cx1, float cy1, float cx2, float cy2) {
    this(x1, y1, x2, y2, cx1, cy1, cx2, cy2, false);
  }

  public CubicCurveShape(float x1, float y1, float x2, float y2,
			 boolean createComp) {
    this(x1, y1, x2, y2, 0, 0, 0, 0, createComp);
    adjustControlPoints();
  }

  public CubicCurveShape(float x1, float y1, float x2, float y2, 
			 float cx1, float cy1, float cx2, float cy2, 
			 boolean createComp) {
    super(createComp);
    p1.setLocation(x1, y1);
    p2.setLocation(x2, y2);
    // ctr1.setLocation(cx1, cy1);
    // ctr2.setLocation(cx2, cy2);
    ctr1 = new Point2DFloat(cx1, cy1);
    ctr2 = new Point2DFloat(cx2, cy2);
    calculateBounds();
  }

  public DrawShape makeInstance() {
    return new CubicCurveShape(comp != null);
  }

  @Override
  public DrawShape cloneDrawShape(boolean deepClone) 
    throws CloneNotSupportedException {
    CubicCurveShape s = new CubicCurveShape((float) p1.getX(), (float) p1.getY(), 
					    (float) p2.getX(), (float) p2.getY(), 
					    (float) ctr1.getX(), (float) ctr1.getY(), 
					    (float) ctr2.getX(), (float) ctr2.getY(),
					    comp != null);
    copyTo(s);
    cloneSubshapes(s, deepClone);
    cloneModel(s);
    return s;
  }

  @Override
  public void copyTo(DrawShapeBase s) 
    throws CloneNotSupportedException {
    if (s != null) {
      super.copyTo(s);
      if (s instanceof CubicCurveShape) {
	CubicCurveShape curve = (CubicCurveShape) s;
	curve.curveType = this.curveType;
      }
    }
  }

  @Override
  public String getName() {
    return "Cubic curve";
  }

  public int getCurveType() {
    return curveType;
  }

  public void setCurveType(int curveType) {
    this.curveType = curveType;
  }

  @Override
  public void moveEnd1Adj(float dx, float dy) {
    p1.setLocation(p1.getX() + dx, p1.getY() + dy);
    if (ctr1 != null) {
      ctr1.setLocation(ctr1.getX() + dx, ctr1.getY() + dy);
    }
    curve = null;
    calculateBounds();
  }

  @Override
  public void moveEnd2Adj(float dx, float dy) {
    p2.setLocation(p2.getX() + dx, p2.getY() + dy);
    if (ctr2 != null) {
      ctr2.setLocation(ctr2.getX() + dx, ctr2.getY() + dy);
    }
    curve = null;
    calculateBounds();
  }

  @Override
  public void draw(Graphics2D g2) {
    if (head != null || tail != null) {
      setArrowPosition();
      float x1 = (float) p1.getX();
      float y1 = (float) p1.getY();
      float cx1 = (float) ctr1.getX();
      float cy1 = (float) ctr1.getY();
      float cx2 = (float) ctr2.getX();
      float cy2 = (float) ctr2.getY();
      float x2 = (float) p2.getX();
      float y2 = (float) p2.getY();
      double d = Point2D.distance(x1, y1, x2, y2);
      if (head != null) {
	// head.getWidth() < d / 2) {
	head.draw(g2);
	Point2D p = head.getTailPosition();
	x2 = (float) p.getX();
	y2 = (float) p.getY();
      }
      if (tail != null) {
	// tail.getWidth() < d / 2) {
	tail.draw(g2);
	Point2D p = tail.getTailPosition();
	x1 = (float) p.getX();
	y1 = (float) p.getY();
      }
      g2.draw(new CubicCurve2D.Float(x1, y1, cx1, cy1, cx2, cy2, x2, y2));
    } else {
      if (curve == null) {
	makeCurve();
      }
      g2.draw(curve);
    }
  }

  @Override
  public void drawOutline(Graphics2D g2, float x1, float y1, float x2, float y2) {
    float cx1, cy1, cx2, cy2;
    if (curveType == C_SHAPE_CURVE) {
      cx1 = x1 + (y2 - y1);
      cy1 = y1 + (x2 - x1);
      cx2 = x2 + (y2 - y1);
      cy2 = y2 + (x2 - x1);
    } else if (curveType == C_SHAPE_CURVE_INVERSE) {
      cx1 = x1 - (y2 - y1);
      cy1 = y1 - (x2 - x1);
      cx2 = x2 - (y2 - y1);
      cy2 = y2 - (x2 - x1);
    } else if (curveType == S_SHAPE_CURVE_INVERSE) {
      cx1 = (x1 + x2) / 2 - (x2 - x1) / 4;
      cy1 = (y1 + y2) / 2 + (y2 - y1) / 4;
      cx2 = (x1 + x2) / 2 + (x2 - x1) / 4;
      cy2 = (y1 + y2) / 2 - (y2 - y1) / 4;
    } else {
      // curveType == S_SHAPE_CURVE
      cx1 = (x1 + x2) / 2 + (x2 - x1) / 4;
      cy1 = (y1 + y2) / 2 - (y2 - y1) / 4;
      cx2 = (x1 + x2) / 2 - (x2 - x1) / 4;
      cy2 = (y1 + y2) / 2 + (y2 - y1) / 4;
    }
    DrawAttr attr = getDrawAttr();
    if (attr != null) {
      g2.setColor(attr.getPenColor());
    } else {
      g2.setColor(DrawAttr.getDefaultPenColor());
    }
    g2.draw(new CubicCurve2D.Float(x1, y1, cx1, cy1, cx2, cy2, x2, y2));
  }

  @Override
  public void drawFrame(Graphics2D g2) {
    int penSize = 1;
    DrawAttr attr = getDrawAttr();
    if (attr != null) {
      penSize = attr.getPenSize();
    }
    int marksize = DrawingUtilities.getMarkSize(penSize);
    float md = marksize / 2.0f;
    float x1 = (float) p1.getX();
    float y1 = (float) p1.getY();
    float cx1 = (float) ctr1.getX();
    float cy1 = (float) ctr1.getY();
    float cx2 = (float) ctr2.getX();
    float cy2 = (float) ctr2.getY();
    float x2 = (float) p2.getX();
    float y2 = (float) p2.getY();
    Color saveColor = g2.getColor();
    Stroke saveStroke = g2.getStroke();

    g2.setColor(anchorColor);
    g2.setStroke(markStroke);
    Ellipse2D e = new Ellipse2D.Float(cx1 - md, cy1 - md, marksize, marksize);
    if (frameMarkFilled) { 
      g2.fill(e);
    } else { 
      g2.draw(e);
    }
    e = new Ellipse2D.Float(cx2 - md, cy2 - md, marksize, marksize);
    if (frameMarkFilled) { 
      g2.fill(e);
    } else { 
      g2.draw(e);
    }

    g2.setStroke(ctrStroke);
    g2.draw(new Line2D.Float(cx1, cy1, x1, y1));
    g2.draw(new Line2D.Float(cx2, cy2, x2, y2));

    g2.setStroke(markStroke);
    g2.setColor(edgeSourceColor);
    e = new Ellipse2D.Float(x1 - md, y1 - md, marksize, marksize);
    if (frameMarkFilled) { 
      g2.fill(e);
    } else { 
      g2.draw(e);
    }

    g2.setColor(edgeDestinationColor);
    e = new Ellipse2D.Float(x2 - md, y2 - md, marksize, marksize);
    if (frameMarkFilled) { 
      g2.fill(e);
    } else { 
      g2.draw(e);
    }

    g2.setColor(saveColor);
    g2.setStroke(saveStroke);
  }

  @Override
  public void setEnds(float x1, float y1, float x2, float y2) {
    p1.setLocation(x1, y1);
    p2.setLocation(x2, y2);
  }

  @Override
  public void setEndsAdj(float x1, float y1, float x2, float y2) {
    p1.setLocation(x1, y1);
    p2.setLocation(x2, y2);
    adjustControlPoints();
    curve = null;
    calculateBounds();
  }

  @Override
  public int isInside(float px, float py) {
    float d1 = d;
    if (attr != null) {
      int penSize = attr.getPenSize();
      if (penSize > 1) {
	d1 += (penSize / 2);
      }
    }
    int result = PointOutside;
    double x1 = bounds.getX();
    double y1 = bounds.getY();
    double x2 = (bounds.getX() + bounds.getWidth());
    double y2 = (bounds.getY() + bounds.getHeight());
    if ((px >= x1 - d1) && 
	(px <= x2 + d1) && 
	(py >= y1 - d1)	&& 
	(py <= y2 + d1)) {
      result = PointInBounds;
      if (isNearPoint((float) p1.getX(), (float) p1.getY(), px, py)) {
	result = PointOnCurveEndPoint + PointGraphEdgeSource;
      } else if (isNearPoint((float) p2.getX(), (float) p2.getY(), px, py)) {
	result = PointOnCurveEndPoint + PointGraphEdgeDestination;
      } else if (ctr1 != null && 
		 isNearPoint((float) ctr1.getX(), (float) ctr1.getY(), px, py)) {
	result = PointOnCurveCtrPoint;
      } else if (ctr2 != null && 
		 isNearPoint((float) ctr2.getX(), (float) ctr2.getY(), px, py)) {
	result = PointOnCurveCtrPoint + 1;
      //} else if (curve.intersects(px - 2, py - 2, 4, 4)) {
      } else if (isOnCurveBoundary(px, py, d1)) {
	result = PointOnCurve;
      }
    }
    return result;
  }

  @Override
  public void setArrowPosition() {
    float x1 = (float) p1.getX();
    float y1 = (float) p1.getY();
    float cx1 = (float) ctr1.getX();
    float cy1 = (float) ctr1.getY();
    float cx2 = (float) ctr2.getX();
    float cy2 = (float) ctr2.getY();
    float x2 = (float) p2.getX();
    float y2 = (float) p2.getY();
    if (head != null) {
      head.setPosition(x2, y2);
      head.setAngle((float) DrawingUtilities.calculateRotationAngle(x2, y2, cx2, cy2));
    }
    if (tail != null) {
      tail.setPosition(x1, y1);
      tail.setAngle((float) DrawingUtilities.calculateRotationAngle(x1, y1, cx1, cy1));
    }
  }

  @Override
  public void adjustControlPoints() {
    float x1 = (float) p1.getX();
    float y1 = (float) p1.getY();
    float x2 = (float) p2.getX();
    float y2 = (float) p2.getY();
    float cx1, cy1, cx2, cy2;
    if (curveType == C_SHAPE_CURVE) {
      cx1 = x1 + (y2 - y1);
      cy1 = y1 + (x2 - x1);
      cx2 = x2 + (y2 - y1);
      cy2 = y2 + (x2 - x1);
    } else if (curveType == C_SHAPE_CURVE_INVERSE) {
      cx1 = x1 - (y2 - y1);
      cy1 = y1 - (x2 - x1);
      cx2 = x2 - (y2 - y1);
      cy2 = y2 - (x2 - x1);
    } else if (curveType == S_SHAPE_CURVE_INVERSE) {
      cx1 = (x1 + x2) / 2 - (x2 - x1) / 4;
      cy1 = (y1 + y2) / 2 + (y2 - y1) / 4;
      cx2 = (x1 + x2) / 2 + (x2 - x1) / 4;
      cy2 = (y1 + y2) / 2 - (y2 - y1) / 4;
    } else {
      // curveType == S_SHAPE_CURVE
      cx1 = (x1 + x2) / 2 + (x2 - x1) / 4;
      cy1 = (y1 + y2) / 2 - (y2 - y1) / 4;
      cx2 = (x1 + x2) / 2 - (x2 - x1) / 4;
      cy2 = (y1 + y2) / 2 + (y2 - y1) / 4;
    }
    setCtr1(cx1, cy1);
    setCtr2(cx2, cy2);
    curve = null;
    calculateBounds();
  }

  @Override
  public void adjustEnds(GraphNodeBase src, GraphNodeBase des, 
			 boolean adjustCtrPts) {
    Point2D q1, q2;
    if (src != null) {
      q1 = src.intersect(ctr1, 
			 tailOffset.dx * src.getShapeWidth(),
			 tailOffset.dy * src.getShapeHeight());
      //CHECK(q1);
    } else {
      q1 = p1;
    }
    if (des != null) {
      q2 = des.intersect(ctr2, 
			 headOffset.dx * des.getShapeWidth(),
			 headOffset.dy * des.getShapeHeight());
      //CHECK(q2);
    } else {
      q2 = p2;
    }

    if (q1 != null && q2 != null) {
      float qx1 = (float) q1.getX();
      float qy1 = (float) q1.getY();
      float qx2 = (float) q2.getX();
      float qy2 = (float) q2.getY();
      if (adjustCtrPts) {
	// keep control points at the same location relative to the end points
	float cx1 = (float) ctr1.getX();
	float cy1 = (float) ctr1.getY();
	float cx2 = (float) ctr2.getX();
	float cy2 = (float) ctr2.getY();
	float x1 = (float) p1.getX();
	float y1 = (float) p1.getY();
	float x2 = (float) p2.getX();
	float y2 = (float) p2.getY();
	ctr1.setLocation(cx1 + (qx1 - x1), cy1 + (qy1 - y1));
	ctr2.setLocation(cx2 + (qx2 - x2), cy2 + (qy2 - y2));
      }

      //CHECK(ctr1);
      //CHECK(ctr2);

      setEnds(qx1, qy1, qx2, qy2);
      // setEnds((float) q1.getX(), (float) q1.getY(), (float) q2.getX(),
      // (float) q2.getY());
    }

    curve = null;
    calculateBounds();
  }

  @Override
  public void swapEnds() { 
    super.swapEnds();

    float cx1 = (float) ctr1.getX();
    float cy1 = (float) ctr1.getY();
    float cx2 = (float) ctr2.getX();
    float cy2 = (float) ctr2.getY();

    ctr1.setLocation(cx2, cy2);
    ctr2.setLocation(cx1, cy1);
  }

  @Override
  protected void makeCurve() {
    if (curve == null) {
      float x1 = (float) p1.getX();
      float y1 = (float) p1.getY();
      float cx1 = (float) ctr1.getX();
      float cy1 = (float) ctr1.getY();
      float cx2 = (float) ctr2.getX();
      float cy2 = (float) ctr2.getY();
      float x2 = (float) p2.getX();
      float y2 = (float) p2.getY();
      curve = new CubicCurve2DFloat(x1, y1, cx1, cy1, cx2, cy2, x2, y2);
    }
  }

  @Override
  protected Map<String, Object> getXMLAttributes() {
    Map<String, Object> attrs = super.getXMLAttributes();
    if (attrs == null) {
      attrs = new HashMap<String, Object>();
    }
    attrs.put("ctype", curveType);
    return attrs;
  }

  @Override
  protected void processXMLAttributes(org.w3c.dom.Element node) {
    if (node != null) {
      super.processXMLAttributes(node);

      String ts;
      ts = node.getAttribute("ctype");
      try {
	curveType = Integer.parseInt(ts);
      } catch (NumberFormatException ex) {
      }
    }
  }

}
